package com.way.draglistview;

import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.util.ArrayList;

import android.content.Context;
import android.content.res.TypedArray;
import android.database.DataSetObserver;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Point;
import android.graphics.drawable.Drawable;
import android.os.Environment;
import android.os.SystemClock;
import android.util.AttributeSet;
import android.util.Log;
import android.util.SparseBooleanArray;
import android.util.SparseIntArray;
import android.view.Gravity;
import android.view.MotionEvent;
import android.view.View;
import android.view.ViewGroup;
import android.widget.AbsListView;
import android.widget.BaseAdapter;
import android.widget.ListAdapter;
import android.widget.ListView;

import com.example.text.R;

/**
 * ListView subclass that mediates drag and drop resorting of items.
 * 
 * 
 * @author way
 * 
 */
public class DragSortListView extends ListView {

	/**
	 * The View that floats above the ListView and represents the dragged item.
	 */
	private View mFloatView;

	/**
	 * The float View location. First based on touch location and given deltaX
	 * and deltaY. Then restricted by callback to
	 * FloatViewManager.onDragFloatView(). Finally restricted by bounds of DSLV.
	 */
	private Point mFloatLoc = new Point();

	private Point mTouchLoc = new Point();

	/**
	 * The middle (in the y-direction) of the floating View.
	 */
	private int mFloatViewMid;

	/**
	 * Flag to make sure float View isn't measured twice
	 */
	private boolean mFloatViewOnMeasured = false;

	/**
	 * Watch the Adapter for data changes. Cancel a drag if coincident with a
	 * change.
	 */
	private DataSetObserver mObserver;

	/**
	 * Transparency for the floating View (XML attribute).
	 */
	private float mFloatAlpha = 1.0f;
	private float mCurrFloatAlpha = 1.0f;

	/**
	 * While drag-sorting, the current position of the floating View. If
	 * dropped, the dragged item will land in this position.
	 */
	private int mFloatPos;

	/**
	 * The first expanded ListView position that helps represent the drop slot
	 * tracking the floating View.
	 */
	private int mFirstExpPos;

	/**
	 * The second expanded ListView position that helps represent the drop slot
	 * tracking the floating View. This can equal mFirstExpPos if there is no
	 * slide shuffle occurring; otherwise it is equal to mFirstExpPos + 1.
	 */
	private int mSecondExpPos;

	/**
	 * Flag set if slide shuffling is enabled.
	 */
	private boolean mAnimate = false;

	/**
	 * The user dragged from this position.
	 */
	private int mSrcPos;

	/**
	 * Offset (in x) within the dragged item at which the user picked it up (or
	 * first touched down with the digitalis).
	 */
	private int mDragDeltaX;

	/**
	 * Offset (in y) within the dragged item at which the user picked it up (or
	 * first touched down with the digitalis).
	 */
	private int mDragDeltaY;

	/**
	 * The difference (in x) between screen coordinates and coordinates in this
	 * view.
	 */
	private int mOffsetX;

	/**
	 * The difference (in y) between screen coordinates and coordinates in this
	 * view.
	 */
	private int mOffsetY;

	/**
	 * A listener that receives callbacks whenever the floating View hovers over
	 * a new position.
	 */
	private DragListener mDragListener;

	/**
	 * A listener that receives a callback when the floating View is dropped.
	 */
	private DropListener mDropListener;

	/**
	 * A listener that receives a callback when the floating View (or more
	 * precisely the originally dragged item) is removed by one of the provided
	 * gestures.
	 */
	private RemoveListener mRemoveListener;

	/**
	 * Enable/Disable item dragging
	 * 
	 * @attr name dslv:drag_enabled
	 */
	private boolean mDragEnabled = true;

	/**
	 * Drag state enum.
	 */
	private final static int IDLE = 0;
	private final static int REMOVING = 1;
	private final static int DROPPING = 2;
	private final static int STOPPED = 3;
	private final static int DRAGGING = 4;

	private int mDragState = IDLE;

	/**
	 * Height in pixels to which the originally dragged item is collapsed during
	 * a drag-sort. Currently, this value must be greater than zero.
	 */
	private int mItemHeightCollapsed = 1;

	/**
	 * Height of the floating View. Stored for the purpose of providing the
	 * tracking drop slot.
	 */
	private int mFloatViewHeight;

	/**
	 * Convenience member. See above.
	 */
	private int mFloatViewHeightHalf;

	/**
	 * Save the given width spec for use in measuring children
	 */
	private int mWidthMeasureSpec = 0;

	/**
	 * Sample Views ultimately used for calculating the height of ListView items
	 * that are off-screen.
	 */
	private View[] mSampleViewTypes = new View[1];

	/**
	 * Drag-scroll encapsulator!
	 */
	private DragScroller mDragScroller;

	/**
	 * Determines the start of the upward drag-scroll region at the top of the
	 * ListView. Specified by a fraction of the ListView height, thus screen
	 * resolution agnostic.
	 */
	private float mDragUpScrollStartFrac = 1.0f / 3.0f;

	/**
	 * Determines the start of the downward drag-scroll region at the bottom of
	 * the ListView. Specified by a fraction of the ListView height, thus screen
	 * resolution agnostic.
	 */
	private float mDragDownScrollStartFrac = 1.0f / 3.0f;

	/**
	 * The following are calculated from the above fracs.
	 */
	private int mUpScrollStartY;
	private int mDownScrollStartY;
	private float mDownScrollStartYF;
	private float mUpScrollStartYF;

	/**
	 * Calculated from above above and current ListView height.
	 */
	private float mDragUpScrollHeight;

	/**
	 * Calculated from above above and current ListView height.
	 */
	private float mDragDownScrollHeight;

	/**
	 * Maximum drag-scroll speed in pixels per ms. Only used with default linear
	 * drag-scroll profile.
	 */
	private float mMaxScrollSpeed = 0.5f;

	/**
	 * Defines the scroll speed during a drag-scroll. User can provide their
	 * own; this default is a simple linear profile where scroll speed increases
	 * linearly as the floating View nears the top/bottom of the ListView.
	 */
	private DragScrollProfile mScrollProfile = new DragScrollProfile() {
		@Override
		public float getSpeed(float w, long t) {
			return mMaxScrollSpeed * w;
		}
	};

	/**
	 * Current touch x.
	 */
	private int mX;

	/**
	 * Current touch y.
	 */
	private int mY;

	/**
	 * Last touch x.
	 */
	private int mLastX;

	/**
	 * Last touch y.
	 */
	private int mLastY;

	/**
	 * The touch y-coord at which drag started
	 */
	private int mDragStartY;

	/**
	 * Drag flag bit. Floating View can move in the positive x direction.
	 */
	public final static int DRAG_POS_X = 0x1;

	/**
	 * Drag flag bit. Floating View can move in the negative x direction.
	 */
	public final static int DRAG_NEG_X = 0x2;

	/**
	 * Drag flag bit. Floating View can move in the positive y direction. This
	 * is subtle. What this actually means is that, if enabled, the floating
	 * View can be dragged below its starting position. Remove in favor of
	 * upper-bounding item position?
	 */
	public final static int DRAG_POS_Y = 0x4;

	/**
	 * Drag flag bit. Floating View can move in the negative y direction. This
	 * is subtle. What this actually means is that the floating View can be
	 * dragged above its starting position. Remove in favor of lower-bounding
	 * item position?
	 */
	public final static int DRAG_NEG_Y = 0x8;

	/**
	 * Flags that determine limits on the motion of the floating View. See flags
	 * above.
	 */
	private int mDragFlags = 0;

	/**
	 * Last call to an on*TouchEvent was a call to onInterceptTouchEvent.
	 */
	private boolean mLastCallWasIntercept = false;

	/**
	 * A touch event is in progress.
	 */
	private boolean mInTouchEvent = false;

	/**
	 * Let the user customize the floating View.
	 */
	private FloatViewManager mFloatViewManager = null;

	/**
	 * Given to ListView to cancel its action when a drag-sort begins.
	 */
	private MotionEvent mCancelEvent;

	/**
	 * Enum telling where to cancel the ListView action when a drag-sort begins
	 */
	private static final int NO_CANCEL = 0;
	private static final int ON_TOUCH_EVENT = 1;
	private static final int ON_INTERCEPT_TOUCH_EVENT = 2;

	/**
	 * Where to cancel the ListView action when a drag-sort begins
	 */
	private int mCancelMethod = NO_CANCEL;

	/**
	 * Determines when a slide shuffle animation starts. That is, defines how
	 * close to the edge of the drop slot the floating View must be to initiate
	 * the slide.
	 */
	private float mSlideRegionFrac = 0.25f;

	/**
	 * Number between 0 and 1 indicating the relative location of a sliding item
	 * (only used if drag-sort animations are turned on). Nearly 1 means the
	 * item is at the top of the slide region (nearly full blank item is
	 * directly below).
	 */
	private float mSlideFrac = 0.0f;

	/**
	 * Wraps the user-provided ListAdapter. This is used to wrap each item View
	 * given by the user inside another View (currenly a RelativeLayout) which
	 * expands and collapses to simulate the item shuffling.
	 */
	private AdapterWrapper mAdapterWrapper;

	/**
	 * Turn on custom debugger.
	 */
	private boolean mTrackDragSort = false;

	/**
	 * Debugging class.
	 */
	private DragSortTracker mDragSortTracker;

	/**
	 * Needed for adjusting item heights from within layoutChildren
	 */
	private boolean mBlockLayoutRequests = false;

	/**
	 * Set to true when a down event happens during drag sort; for example, when
	 * drag finish animations are playing.
	 */
	private boolean mIgnoreTouchEvent = false;

	/**
	 * Caches DragSortItemView child heights. Sometimes DSLV has to know the
	 * height of an offscreen item. Since ListView virtualizes these, DSLV must
	 * get the item from the ListAdapter to obtain its height. That process can
	 * be expensive, but often the same offscreen item will be requested many
	 * times in a row. Once an offscreen item height is calculated, we cache it
	 * in this guy. Actually, we cache the height of the child of the
	 * DragSortItemView since the item height changes often during a drag-sort.
	 */
	private static final int sCacheSize = 3;
	private HeightCache mChildHeightCache = new HeightCache(sCacheSize);

	private RemoveAnimator mRemoveAnimator;

	private LiftAnimator mLiftAnimator;

	private DropAnimator mDropAnimator;

	private boolean mUseRemoveVelocity;
	private float mRemoveVelocityX = 0;

	public DragSortListView(Context context, AttributeSet attrs) {
		super(context, attrs);

		int defaultDuration = 150;
		int removeAnimDuration = defaultDuration; // ms
		int dropAnimDuration = defaultDuration; // ms

		if (attrs != null) {
			TypedArray a = getContext().obtainStyledAttributes(attrs,
					R.styleable.DragSortListView, 0, 0);

			mItemHeightCollapsed = Math.max(1, a.getDimensionPixelSize(
					R.styleable.DragSortListView_collapsed_height, 1));

			mTrackDragSort = a.getBoolean(
					R.styleable.DragSortListView_track_drag_sort, false);

			if (mTrackDragSort) {
				mDragSortTracker = new DragSortTracker();
			}

			// alpha between 0 and 255, 0=transparent, 255=opaque
			mFloatAlpha = a.getFloat(R.styleable.DragSortListView_float_alpha,
					mFloatAlpha);
			mCurrFloatAlpha = mFloatAlpha;

			mDragEnabled = a.getBoolean(
					R.styleable.DragSortListView_drag_enabled, mDragEnabled);

			mSlideRegionFrac = Math.max(0.0f, Math.min(1.0f, 1.0f - a.getFloat(
					R.styleable.DragSortListView_slide_shuffle_speed, 0.75f)));

			mAnimate = mSlideRegionFrac > 0.0f;

			float frac = a.getFloat(
					R.styleable.DragSortListView_drag_scroll_start,
					mDragUpScrollStartFrac);

			setDragScrollStart(frac);

			mMaxScrollSpeed = a.getFloat(
					R.styleable.DragSortListView_max_drag_scroll_speed,
					mMaxScrollSpeed);

			removeAnimDuration = a.getInt(
					R.styleable.DragSortListView_remove_animation_duration,
					removeAnimDuration);

			dropAnimDuration = a.getInt(
					R.styleable.DragSortListView_drop_animation_duration,
					dropAnimDuration);

			boolean useDefault = a.getBoolean(
					R.styleable.DragSortListView_use_default_controller, true);

			if (useDefault) {
				boolean removeEnabled = a.getBoolean(
						R.styleable.DragSortListView_remove_enabled, false);
				int removeMode = a.getInt(
						R.styleable.DragSortListView_remove_mode,
						DragSortController.FLING_REMOVE);
				boolean sortEnabled = a.getBoolean(
						R.styleable.DragSortListView_sort_enabled, true);
				int dragInitMode = a.getInt(
						R.styleable.DragSortListView_drag_start_mode,
						DragSortController.ON_DOWN);
				int dragHandleId = a.getResourceId(
						R.styleable.DragSortListView_drag_handle_id, 0);
				int flingHandleId = a.getResourceId(
						R.styleable.DragSortListView_fling_handle_id, 0);
				int clickRemoveId = a.getResourceId(
						R.styleable.DragSortListView_click_remove_id, 0);
				int bgColor = a.getColor(
						R.styleable.DragSortListView_float_background_color,
						Color.BLACK);

				DragSortController controller = new DragSortController(this,
						dragHandleId, dragInitMode, removeMode, clickRemoveId,
						flingHandleId);
				controller.setRemoveEnabled(removeEnabled);
				controller.setSortEnabled(sortEnabled);
				controller.setBackgroundColor(bgColor);

				mFloatViewManager = controller;
				setOnTouchListener(controller);
			}

			a.recycle();
		}

		mDragScroller = new DragScroller();

		float smoothness = 0.5f;
		if (removeAnimDuration > 0) {
			mRemoveAnimator = new RemoveAnimator(smoothness, removeAnimDuration);
		}
		// mLiftAnimator = new LiftAnimator(smoothness, 100);
		if (dropAnimDuration > 0) {
			mDropAnimator = new DropAnimator(smoothness, dropAnimDuration);
		}

		mCancelEvent = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f,
				0f, 0f, 0f, 0, 0f, 0f, 0, 0);

		// construct the dataset observer
		mObserver = new DataSetObserver() {
			private void cancel() {
				if (mDragState == DRAGGING) {
					cancelDrag();
				}
			}

			@Override
			public void onChanged() {
				cancel();
			}

			@Override
			public void onInvalidated() {
				cancel();
			}
		};
	}

	/**
	 * Usually called from a FloatViewManager. The float alpha will be reset to
	 * the xml-defined value every time a drag is stopped.
	 */
	public void setFloatAlpha(float alpha) {
		mCurrFloatAlpha = alpha;
	}

	public float getFloatAlpha() {
		return mCurrFloatAlpha;
	}

	/**
	 * Set maximum drag scroll speed in positions/second. Only applies if using
	 * default ScrollSpeedProfile.
	 * 
	 * @param max
	 *            Maximum scroll speed.
	 */
	public void setMaxScrollSpeed(float max) {
		mMaxScrollSpeed = max;
	}

	/**
	 * For each DragSortListView Listener interface implemented by
	 * <code>adapter</code>, this method calls the appropriate set*Listener
	 * method with <code>adapter</code> as the argument.
	 * 
	 * @param adapter
	 *            The ListAdapter providing data to back DragSortListView.
	 * 
	 * @see android.widget.ListView#setAdapter(android.widget.ListAdapter)
	 */
	@Override
	public void setAdapter(ListAdapter adapter) {
		if (adapter != null) {
			mAdapterWrapper = new AdapterWrapper(adapter);
			adapter.registerDataSetObserver(mObserver);

			if (adapter instanceof DropListener) {
				setDropListener((DropListener) adapter);
			}
			if (adapter instanceof DragListener) {
				setDragListener((DragListener) adapter);
			}
			if (adapter instanceof RemoveListener) {
				setRemoveListener((RemoveListener) adapter);
			}
		} else {
			mAdapterWrapper = null;
		}

		super.setAdapter(mAdapterWrapper);
	}

	/**
	 * As opposed to {@link ListView#getAdapter()}, which returns a heavily
	 * wrapped ListAdapter (DragSortListView wraps the input ListAdapter {\emph
	 * and} ListView wraps the wrapped one).
	 * 
	 * @return The ListAdapter set as the argument of {@link setAdapter()}
	 */
	public ListAdapter getInputAdapter() {
		if (mAdapterWrapper == null) {
			return null;
		} else {
			return mAdapterWrapper.getAdapter();
		}
	}

	private class AdapterWrapper extends BaseAdapter {
		private ListAdapter mAdapter;

		public AdapterWrapper(ListAdapter adapter) {
			super();
			mAdapter = adapter;

			mAdapter.registerDataSetObserver(new DataSetObserver() {
				public void onChanged() {
					notifyDataSetChanged();
				}

				public void onInvalidated() {
					notifyDataSetInvalidated();
				}
			});
		}

		public ListAdapter getAdapter() {
			return mAdapter;
		}

		@Override
		public long getItemId(int position) {
			return mAdapter.getItemId(position);
		}

		@Override
		public Object getItem(int position) {
			return mAdapter.getItem(position);
		}

		@Override
		public int getCount() {
			return mAdapter.getCount();
		}

		@Override
		public boolean areAllItemsEnabled() {
			return mAdapter.areAllItemsEnabled();
		}

		@Override
		public boolean isEnabled(int position) {
			return mAdapter.isEnabled(position);
		}

		@Override
		public int getItemViewType(int position) {
			return mAdapter.getItemViewType(position);
		}

		@Override
		public int getViewTypeCount() {
			return mAdapter.getViewTypeCount();
		}

		@Override
		public boolean hasStableIds() {
			return mAdapter.hasStableIds();
		}

		@Override
		public boolean isEmpty() {
			return mAdapter.isEmpty();
		}

		@Override
		public View getView(int position, View convertView, ViewGroup parent) {

			DragSortItemView v;
			View child;
			// Log.d("mobeta",
			// "getView: position="+position+" convertView="+convertView);
			if (convertView != null) {
				v = (DragSortItemView) convertView;
				View oldChild = v.getChildAt(0);

				child = mAdapter.getView(position, oldChild,
						DragSortListView.this);
				if (child != oldChild) {
					// shouldn't get here if user is reusing convertViews
					// properly
					if (oldChild != null) {
						v.removeViewAt(0);
					}
					v.addView(child);
				}
			} else {
				child = mAdapter.getView(position, null, DragSortListView.this);
				// if (child instanceof Checkable) {
				// v = new DragSortItemViewCheckable(getContext());
				// } else {
				v = new DragSortItemView(getContext());
				// }
				v.setLayoutParams(new AbsListView.LayoutParams(
						ViewGroup.LayoutParams.FILL_PARENT,
						ViewGroup.LayoutParams.WRAP_CONTENT));
				v.addView(child);
			}

			// Set the correct item height given drag state; passed
			// View needs to be measured if measurement is required.
			adjustItem(position + getHeaderViewsCount(), v, true);

			return v;
		}
	}

	private void drawDivider(int expPosition, Canvas canvas) {

		final Drawable divider = getDivider();
		final int dividerHeight = getDividerHeight();
		// Log.d("mobeta", "div="+divider+" divH="+dividerHeight);

		if (divider != null && dividerHeight != 0) {
			final ViewGroup expItem = (ViewGroup) getChildAt(expPosition
					- getFirstVisiblePosition());
			if (expItem != null) {
				final int l = getPaddingLeft();
				final int r = getWidth() - getPaddingRight();
				final int t;
				final int b;

				final int childHeight = expItem.getChildAt(0).getHeight();

				if (expPosition > mSrcPos) {
					t = expItem.getTop() + childHeight;
					b = t + dividerHeight;
				} else {
					b = expItem.getBottom() - childHeight;
					t = b - dividerHeight;
				}
				// Log.d("mobeta", "l="+l+" t="+t+" r="+r+" b="+b);

				// Have to clip to support ColorDrawable on <= Gingerbread
				canvas.save();
				canvas.clipRect(l, t, r, b);
				divider.setBounds(l, t, r, b);
				divider.draw(canvas);
				canvas.restore();
			}
		}
	}

	@Override
	protected void dispatchDraw(Canvas canvas) {
		super.dispatchDraw(canvas);

		if (mDragState != IDLE) {
			// draw the divider over the expanded item
			if (mFirstExpPos != mSrcPos) {
				drawDivider(mFirstExpPos, canvas);
			}
			if (mSecondExpPos != mFirstExpPos && mSecondExpPos != mSrcPos) {
				drawDivider(mSecondExpPos, canvas);
			}
		}

		if (mFloatView != null) {
			// draw the float view over everything
			final int w = mFloatView.getWidth();
			final int h = mFloatView.getHeight();

			int x = mFloatLoc.x;

			int width = getWidth();
			if (x < 0)
				x = -x;
			float alphaMod;
			if (x < width) {
				alphaMod = ((float) (width - x)) / ((float) width);
				alphaMod *= alphaMod;
			} else {
				alphaMod = 0;
			}

			final int alpha = (int) (255f * mCurrFloatAlpha * alphaMod);

			canvas.save();
			// Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds());
			canvas.translate(mFloatLoc.x, mFloatLoc.y);
			canvas.clipRect(0, 0, w, h);

			// Log.d("mobeta", "clip rect bounds: " + canvas.getClipBounds());
			canvas.saveLayerAlpha(0, 0, w, h, alpha, Canvas.ALL_SAVE_FLAG);
			mFloatView.draw(canvas);
			canvas.restore();
			canvas.restore();
		}
	}

	private int getItemHeight(int position) {
		View v = getChildAt(position - getFirstVisiblePosition());

		if (v != null) {
			// item is onscreen, just get the height of the View
			return v.getHeight();
		} else {
			// item is offscreen. get child height and calculate
			// item height based on current shuffle state
			return calcItemHeight(position, getChildHeight(position));
		}
	}

	private void printPosData() {
		Log.d("mobeta", "mSrcPos=" + mSrcPos + " mFirstExpPos=" + mFirstExpPos
				+ " mSecondExpPos=" + mSecondExpPos);
	}

	private class HeightCache {

		private SparseIntArray mMap;
		private ArrayList<Integer> mOrder;
		private int mMaxSize;

		public HeightCache(int size) {
			mMap = new SparseIntArray(size);
			mOrder = new ArrayList<Integer>(size);
			mMaxSize = size;
		}

		/**
		 * Add item height at position if doesn't already exist.
		 */
		public void add(int position, int height) {
			int currHeight = mMap.get(position, -1);
			if (currHeight != height) {
				if (currHeight == -1) {
					if (mMap.size() == mMaxSize) {
						// remove oldest entry
						mMap.delete(mOrder.remove(0));
					}
				} else {
					// move position to newest slot
					mOrder.remove((Integer) position);
				}
				mMap.put(position, height);
				mOrder.add(position);
			}
		}

		public int get(int position) {
			return mMap.get(position, -1);
		}

		public void clear() {
			mMap.clear();
			mOrder.clear();
		}

	}

	/**
	 * Get the shuffle edge for item at position when top of item is at y-coord
	 * top. Assumes that current item heights are consistent with current float
	 * view location and thus expanded positions and slide fraction. i.e. Should
	 * not be called between update of expanded positions/slide fraction and
	 * layoutChildren.
	 * 
	 * @param position
	 * @param top
	 * @param height
	 *            Height of item at position. If -1, this function calculates
	 *            this height.
	 * 
	 * @return Shuffle line between position-1 and position (for the given view
	 *         of the list; that is, for when top of item at position has
	 *         y-coord of given `top`). If floating View (treated as horizontal
	 *         line) is dropped immediately above this line, it lands in
	 *         position-1. If dropped immediately below this line, it lands in
	 *         position.
	 */
	private int getShuffleEdge(int position, int top) {

		final int numHeaders = getHeaderViewsCount();
		final int numFooters = getFooterViewsCount();

		// shuffle edges are defined between items that can be
		// dragged; there are N-1 of them if there are N draggable
		// items.

		if (position <= numHeaders || (position >= getCount() - numFooters)) {
			return top;
		}

		int divHeight = getDividerHeight();

		int edge;

		int maxBlankHeight = mFloatViewHeight - mItemHeightCollapsed;
		int childHeight = getChildHeight(position);
		int itemHeight = getItemHeight(position);

		// first calculate top of item given that floating View is
		// centered over src position
		int otop = top;
		if (mSecondExpPos <= mSrcPos) {
			// items are expanded on and/or above the source position

			if (position == mSecondExpPos && mFirstExpPos != mSecondExpPos) {
				if (position == mSrcPos) {
					otop = top + itemHeight - mFloatViewHeight;
				} else {
					int blankHeight = itemHeight - childHeight;
					otop = top + blankHeight - maxBlankHeight;
				}
			} else if (position > mSecondExpPos && position <= mSrcPos) {
				otop = top - maxBlankHeight;
			}

		} else {
			// items are expanded on and/or below the source position

			if (position > mSrcPos && position <= mFirstExpPos) {
				otop = top + maxBlankHeight;
			} else if (position == mSecondExpPos
					&& mFirstExpPos != mSecondExpPos) {
				int blankHeight = itemHeight - childHeight;
				otop = top + blankHeight;
			}
		}

		// otop is set
		if (position <= mSrcPos) {
			edge = otop
					+ (mFloatViewHeight - divHeight - getChildHeight(position - 1))
					/ 2;
		} else {
			edge = otop + (childHeight - divHeight - mFloatViewHeight) / 2;
		}

		return edge;
	}

	private boolean updatePositions() {

		final int first = getFirstVisiblePosition();
		int startPos = mFirstExpPos;
		View startView = getChildAt(startPos - first);

		if (startView == null) {
			startPos = first + getChildCount() / 2;
			startView = getChildAt(startPos - first);
		}
		int startTop = startView.getTop();

		int itemHeight = startView.getHeight();

		int edge = getShuffleEdge(startPos, startTop);
		int lastEdge = edge;

		int divHeight = getDividerHeight();

		// Log.d("mobeta", "float mid="+mFloatViewMid);

		int itemPos = startPos;
		int itemTop = startTop;
		if (mFloatViewMid < edge) {
			// scanning up for float position
			// Log.d("mobeta", "    edge="+edge);
			while (itemPos >= 0) {
				itemPos--;
				itemHeight = getItemHeight(itemPos);

				if (itemPos == 0) {
					edge = itemTop - divHeight - itemHeight;
					break;
				}

				itemTop -= itemHeight + divHeight;
				edge = getShuffleEdge(itemPos, itemTop);
				// Log.d("mobeta", "    edge="+edge);

				if (mFloatViewMid >= edge) {
					break;
				}

				lastEdge = edge;
			}
		} else {
			// scanning down for float position
			// Log.d("mobeta", "    edge="+edge);
			final int count = getCount();
			while (itemPos < count) {
				if (itemPos == count - 1) {
					edge = itemTop + divHeight + itemHeight;
					break;
				}

				itemTop += divHeight + itemHeight;
				itemHeight = getItemHeight(itemPos + 1);
				edge = getShuffleEdge(itemPos + 1, itemTop);
				// Log.d("mobeta", "    edge="+edge);

				// test for hit
				if (mFloatViewMid < edge) {
					break;
				}

				lastEdge = edge;
				itemPos++;
			}
		}

		final int numHeaders = getHeaderViewsCount();
		final int numFooters = getFooterViewsCount();

		boolean updated = false;

		int oldFirstExpPos = mFirstExpPos;
		int oldSecondExpPos = mSecondExpPos;
		float oldSlideFrac = mSlideFrac;

		if (mAnimate) {
			int edgeToEdge = Math.abs(edge - lastEdge);

			int edgeTop, edgeBottom;
			if (mFloatViewMid < edge) {
				edgeBottom = edge;
				edgeTop = lastEdge;
			} else {
				edgeTop = edge;
				edgeBottom = lastEdge;
			}
			// Log.d("mobeta", "edgeTop="+edgeTop+" edgeBot="+edgeBottom);

			int slideRgnHeight = (int) (0.5f * mSlideRegionFrac * edgeToEdge);
			float slideRgnHeightF = (float) slideRgnHeight;
			int slideEdgeTop = edgeTop + slideRgnHeight;
			int slideEdgeBottom = edgeBottom - slideRgnHeight;

			// Three regions
			if (mFloatViewMid < slideEdgeTop) {
				mFirstExpPos = itemPos - 1;
				mSecondExpPos = itemPos;
				mSlideFrac = 0.5f * ((float) (slideEdgeTop - mFloatViewMid))
						/ slideRgnHeightF;
				// Log.d("mobeta",
				// "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac);
			} else if (mFloatViewMid < slideEdgeBottom) {
				mFirstExpPos = itemPos;
				mSecondExpPos = itemPos;
			} else {
				mFirstExpPos = itemPos;
				mSecondExpPos = itemPos + 1;
				mSlideFrac = 0.5f * (1.0f + ((float) (edgeBottom - mFloatViewMid))
						/ slideRgnHeightF);
				// Log.d("mobeta",
				// "firstExp="+mFirstExpPos+" secExp="+mSecondExpPos+" slideFrac="+mSlideFrac);
			}

		} else {
			mFirstExpPos = itemPos;
			mSecondExpPos = itemPos;
		}

		// correct for headers and footers
		if (mFirstExpPos < numHeaders) {
			itemPos = numHeaders;
			mFirstExpPos = itemPos;
			mSecondExpPos = itemPos;
		} else if (mSecondExpPos >= getCount() - numFooters) {
			itemPos = getCount() - numFooters - 1;
			mFirstExpPos = itemPos;
			mSecondExpPos = itemPos;
		}

		if (mFirstExpPos != oldFirstExpPos || mSecondExpPos != oldSecondExpPos
				|| mSlideFrac != oldSlideFrac) {
			updated = true;
		}

		if (itemPos != mFloatPos) {
			if (mDragListener != null) {
				mDragListener
						.drag(mFloatPos - numHeaders, itemPos - numHeaders);
			}

			mFloatPos = itemPos;
			updated = true;
		}

		return updated;
	}

	@Override
	protected void onDraw(Canvas canvas) {
		super.onDraw(canvas);

		if (mTrackDragSort) {
			mDragSortTracker.appendState();
		}
	}

	private class SmoothAnimator implements Runnable {
		protected long mStartTime;

		private float mDurationF;

		private float mAlpha;
		private float mA, mB, mC, mD;

		private boolean mCanceled;

		public SmoothAnimator(float smoothness, int duration) {
			mAlpha = smoothness;
			mDurationF = (float) duration;
			mA = mD = 1f / (2f * mAlpha * (1f - mAlpha));
			mB = mAlpha / (2f * (mAlpha - 1f));
			mC = 1f / (1f - mAlpha);
		}

		public float transform(float frac) {
			if (frac < mAlpha) {
				return mA * frac * frac;
			} else if (frac < 1f - mAlpha) {
				return mB + mC * frac;
			} else {
				return 1f - mD * (frac - 1f) * (frac - 1f);
			}
		}

		public void start() {
			mStartTime = SystemClock.uptimeMillis();
			mCanceled = false;
			onStart();
			post(this);
		}

		public void cancel() {
			mCanceled = true;
		}

		public void onStart() {
			// stub
		}

		public void onUpdate(float frac, float smoothFrac) {
			// stub
		}

		public void onStop() {
			// stub
		}

		@Override
		public void run() {
			if (mCanceled) {
				return;
			}

			float fraction = ((float) (SystemClock.uptimeMillis() - mStartTime))
					/ mDurationF;

			if (fraction >= 1f) {
				onUpdate(1f, 1f);
				onStop();
			} else {
				onUpdate(fraction, transform(fraction));
				post(this);
			}
		}
	}

	/**
	 * Centers floating View under touch point.
	 */
	private class LiftAnimator extends SmoothAnimator {

		private float mInitDragDeltaY;
		private float mFinalDragDeltaY;

		public LiftAnimator(float smoothness, int duration) {
			super(smoothness, duration);
		}

		@Override
		public void onStart() {
			mInitDragDeltaY = mDragDeltaY;
			mFinalDragDeltaY = mFloatViewHeightHalf;
		}

		@Override
		public void onUpdate(float frac, float smoothFrac) {
			if (mDragState != DRAGGING) {
				cancel();
			} else {
				mDragDeltaY = (int) (smoothFrac * mFinalDragDeltaY + (1f - smoothFrac)
						* mInitDragDeltaY);
				mFloatLoc.y = mY - mDragDeltaY;
				doDragFloatView(true);
			}
		}
	}

	/**
	 * Centers floating View over drop slot before destroying.
	 */
	private class DropAnimator extends SmoothAnimator {

		private int mDropPos;
		private int srcPos;
		private float mInitDeltaY;
		private float mInitDeltaX;

		public DropAnimator(float smoothness, int duration) {
			super(smoothness, duration);
		}

		@Override
		public void onStart() {
			mDropPos = mFloatPos;
			srcPos = mSrcPos;
			mDragState = DROPPING;
			mInitDeltaY = mFloatLoc.y - getTargetY();
			mInitDeltaX = mFloatLoc.x - getPaddingLeft();
		}

		private int getTargetY() {
			final int first = getFirstVisiblePosition();
			final int otherAdjust = (mItemHeightCollapsed + getDividerHeight()) / 2;
			View v = getChildAt(mDropPos - first);
			int targetY = -1;
			if (v != null) {
				if (mDropPos == srcPos) {
					targetY = v.getTop();
				} else if (mDropPos < srcPos) {
					// expanded down
					targetY = v.getTop() - otherAdjust;
				} else {
					// expanded up
					targetY = v.getBottom() + otherAdjust - mFloatViewHeight;
				}
			} else {
				// drop position is not on screen?? no animation
				cancel();
			}

			return targetY;
		}

		@Override
		public void onUpdate(float frac, float smoothFrac) {
			final int targetY = getTargetY();
			final int targetX = getPaddingLeft();
			final float deltaY = mFloatLoc.y - targetY;
			final float deltaX = mFloatLoc.x - targetX;
			final float f = 1f - smoothFrac;
			if (f < Math.abs(deltaY / mInitDeltaY)
					|| f < Math.abs(deltaX / mInitDeltaX)) {
				mFloatLoc.y = targetY + (int) (mInitDeltaY * f);
				mFloatLoc.x = getPaddingLeft() + (int) (mInitDeltaX * f);
				doDragFloatView(true);
			}
		}

		@Override
		public void onStop() {
			dropFloatView();
		}

	}

	/**
	 * Collapses expanded items.
	 */
	private class RemoveAnimator extends SmoothAnimator {

		private float mFloatLocX;
		private float mFirstStartBlank;
		private float mSecondStartBlank;

		private int mFirstChildHeight = -1;
		private int mSecondChildHeight = -1;

		private int mFirstPos;
		private int mSecondPos;
		private int srcPos;

		public RemoveAnimator(float smoothness, int duration) {
			super(smoothness, duration);
		}

		@Override
		public void onStart() {
			mFirstChildHeight = -1;
			mSecondChildHeight = -1;
			mFirstPos = mFirstExpPos;
			mSecondPos = mSecondExpPos;
			srcPos = mSrcPos;
			mDragState = REMOVING;

			mFloatLocX = mFloatLoc.x;
			if (mUseRemoveVelocity) {
				float minVelocity = 2f * getWidth();
				if (mRemoveVelocityX == 0) {
					mRemoveVelocityX = (mFloatLocX < 0 ? -1 : 1) * minVelocity;
				} else {
					minVelocity *= 2;
					if (mRemoveVelocityX < 0 && mRemoveVelocityX > -minVelocity)
						mRemoveVelocityX = -minVelocity;
					else if (mRemoveVelocityX > 0
							&& mRemoveVelocityX < minVelocity)
						mRemoveVelocityX = minVelocity;
				}
			} else {
				destroyFloatView();
			}
		}

		@Override
		public void onUpdate(float frac, float smoothFrac) {
			float f = 1f - smoothFrac;

			final int firstVis = getFirstVisiblePosition();
			View item = getChildAt(mFirstPos - firstVis);
			ViewGroup.LayoutParams lp;
			int blank;

			if (mUseRemoveVelocity) {
				float dt = (float) (SystemClock.uptimeMillis() - mStartTime) / 1000;
				if (dt == 0)
					return;
				float dx = mRemoveVelocityX * dt;
				int w = getWidth();
				mRemoveVelocityX += (mRemoveVelocityX > 0 ? 1 : -1) * dt * w;
				mFloatLocX += dx;
				mFloatLoc.x = (int) mFloatLocX;
				if (mFloatLocX < w && mFloatLocX > -w) {
					mStartTime = SystemClock.uptimeMillis();
					doDragFloatView(true);
					return;
				}
			}

			if (item != null) {
				if (mFirstChildHeight == -1) {
					mFirstChildHeight = getChildHeight(mFirstPos, item, false);
					mFirstStartBlank = (float) (item.getHeight() - mFirstChildHeight);
				}
				blank = Math.max((int) (f * mFirstStartBlank), 1);
				lp = item.getLayoutParams();
				lp.height = mFirstChildHeight + blank;
				item.setLayoutParams(lp);
			}
			if (mSecondPos != mFirstPos) {
				item = getChildAt(mSecondPos - firstVis);
				if (item != null) {
					if (mSecondChildHeight == -1) {
						mSecondChildHeight = getChildHeight(mSecondPos, item,
								false);
						mSecondStartBlank = (float) (item.getHeight() - mSecondChildHeight);
					}
					blank = Math.max((int) (f * mSecondStartBlank), 1);
					lp = item.getLayoutParams();
					lp.height = mSecondChildHeight + blank;
					item.setLayoutParams(lp);
				}
			}
		}

		@Override
		public void onStop() {
			doRemoveItem();
		}
	}

	public void removeItem(int which) {

		mUseRemoveVelocity = false;
		removeItem(which, 0);
	}

	/**
	 * Removes an item from the list and animates the removal.
	 * 
	 * @param which
	 *            Position to remove (NOTE: headers/footers ignored! this is a
	 *            position in your input ListAdapter).
	 * @param velocityX
	 */
	public void removeItem(int which, float velocityX) {
		if (mDragState == IDLE || mDragState == DRAGGING) {

			if (mDragState == IDLE) {
				// called from outside drag-sort
				mSrcPos = getHeaderViewsCount() + which;
				mFirstExpPos = mSrcPos;
				mSecondExpPos = mSrcPos;
				mFloatPos = mSrcPos;
				View v = getChildAt(mSrcPos - getFirstVisiblePosition());
				if (v != null) {
					v.setVisibility(View.INVISIBLE);
				}
			}

			mDragState = REMOVING;
			mRemoveVelocityX = velocityX;

			if (mInTouchEvent) {
				switch (mCancelMethod) {
				case ON_TOUCH_EVENT:
					super.onTouchEvent(mCancelEvent);
					break;
				case ON_INTERCEPT_TOUCH_EVENT:
					super.onInterceptTouchEvent(mCancelEvent);
					break;
				}
			}

			if (mRemoveAnimator != null) {
				mRemoveAnimator.start();
			} else {
				doRemoveItem(which);
			}
		}
	}

	/**
	 * Move an item, bypassing the drag-sort process. Simply calls through to
	 * {@link DropListener#drop(int, int)}.
	 * 
	 * @param from
	 *            Position to move (NOTE: headers/footers ignored! this is a
	 *            position in your input ListAdapter).
	 * @param to
	 *            Target position (NOTE: headers/footers ignored! this is a
	 *            position in your input ListAdapter).
	 */
	public void moveItem(int from, int to) {
		if (mDropListener != null) {
			final int count = getInputAdapter().getCount();
			if (from >= 0 && from < count && to >= 0 && to < count) {
				mDropListener.drop(from, to);
			}
		}
	}

	/**
	 * Cancel a drag. Calls {@link #stopDrag(boolean, boolean)} with
	 * <code>true</code> as the first argument.
	 */
	public void cancelDrag() {
		if (mDragState == DRAGGING) {
			mDragScroller.stopScrolling(true);
			destroyFloatView();
			clearPositions();
			adjustAllItems();

			if (mInTouchEvent) {
				mDragState = STOPPED;
			} else {
				mDragState = IDLE;
			}
		}
	}

	private void clearPositions() {
		mSrcPos = -1;
		mFirstExpPos = -1;
		mSecondExpPos = -1;
		mFloatPos = -1;
	}

	private void dropFloatView() {
		// must set to avoid cancelDrag being called from the
		// DataSetObserver
		mDragState = DROPPING;

		if (mDropListener != null && mFloatPos >= 0 && mFloatPos < getCount()) {
			final int numHeaders = getHeaderViewsCount();
			mDropListener.drop(mSrcPos - numHeaders, mFloatPos - numHeaders);
		}

		destroyFloatView();

		adjustOnReorder();
		clearPositions();
		adjustAllItems();

		// now the drag is done
		if (mInTouchEvent) {
			mDragState = STOPPED;
		} else {
			mDragState = IDLE;
		}
	}

	private void doRemoveItem() {
		doRemoveItem(mSrcPos - getHeaderViewsCount());
	}

	/**
	 * Removes dragged item from the list. Calls RemoveListener.
	 */
	private void doRemoveItem(int which) {
		// must set to avoid cancelDrag being called from the
		// DataSetObserver
		mDragState = REMOVING;

		// end it
		if (mRemoveListener != null) {
			mRemoveListener.remove(which);
		}

		destroyFloatView();

		adjustOnReorder();
		clearPositions();

		// now the drag is done
		if (mInTouchEvent) {
			mDragState = STOPPED;
		} else {
			mDragState = IDLE;
		}
	}

	private void adjustOnReorder() {
		final int firstPos = getFirstVisiblePosition();
		// Log.d("mobeta", "first="+firstPos+" src="+mSrcPos);
		if (mSrcPos < firstPos) {
			// collapsed src item is off screen;
			// adjust the scroll after item heights have been fixed
			View v = getChildAt(0);
			int top = 0;
			if (v != null) {
				top = v.getTop();
			}
			// Log.d("mobeta", "top="+top+" fvh="+mFloatViewHeight);
			setSelectionFromTop(firstPos - 1, top - getPaddingTop());
		}
	}

	/**
	 * Stop a drag in progress. Pass <code>true</code> if you would like to
	 * remove the dragged item from the list.
	 * 
	 * @param remove
	 *            Remove the dragged item from the list. Calls a registered
	 *            RemoveListener, if one exists. Otherwise, calls the
	 *            DropListener, if one exists.
	 * 
	 * @return True if the stop was successful. False if there is no floating
	 *         View.
	 */
	public boolean stopDrag(boolean remove) {
		mUseRemoveVelocity = false;
		return stopDrag(remove, 0);
	}

	public boolean stopDragWithVelocity(boolean remove, float velocityX) {

		mUseRemoveVelocity = true;
		return stopDrag(remove, velocityX);
	}

	public boolean stopDrag(boolean remove, float velocityX) {
		if (mFloatView != null) {
			mDragScroller.stopScrolling(true);

			if (remove) {
				removeItem(mSrcPos - getHeaderViewsCount(), velocityX);
			} else {
				if (mDropAnimator != null) {
					mDropAnimator.start();
				} else {
					dropFloatView();
				}
			}

			if (mTrackDragSort) {
				mDragSortTracker.stopTracking();
			}

			return true;
		} else {
			// stop failed
			return false;
		}
	}

	@Override
	public boolean onTouchEvent(MotionEvent ev) {
		if (mIgnoreTouchEvent) {
			mIgnoreTouchEvent = false;
			return false;
		}

		if (!mDragEnabled) {
			return super.onTouchEvent(ev);
		}

		boolean more = false;

		boolean lastCallWasIntercept = mLastCallWasIntercept;
		mLastCallWasIntercept = false;

		if (!lastCallWasIntercept) {
			saveTouchCoords(ev);
		}

		// if (mFloatView != null) {
		if (mDragState == DRAGGING) {
			onDragTouchEvent(ev);
			more = true; // give us more!
		} else {
			// what if float view is null b/c we dropped in middle
			// of drag touch event?

			// if (mDragState != STOPPED) {
			if (mDragState == IDLE) {
				if (super.onTouchEvent(ev)) {
					more = true;
				}
			}

			int action = ev.getAction() & MotionEvent.ACTION_MASK;

			switch (action) {
			case MotionEvent.ACTION_CANCEL:
			case MotionEvent.ACTION_UP:
				doActionUpOrCancel();
				break;
			default:
				if (more) {
					mCancelMethod = ON_TOUCH_EVENT;
				}
			}
		}

		return more;
	}

	private void doActionUpOrCancel() {
		mCancelMethod = NO_CANCEL;
		mInTouchEvent = false;
		if (mDragState == STOPPED) {
			mDragState = IDLE;
		}
		mCurrFloatAlpha = mFloatAlpha;
		mListViewIntercepted = false;
		mChildHeightCache.clear();
	}

	private void saveTouchCoords(MotionEvent ev) {
		int action = ev.getAction() & MotionEvent.ACTION_MASK;
		if (action != MotionEvent.ACTION_DOWN) {
			mLastX = mX;
			mLastY = mY;
		}
		mX = (int) ev.getX();
		mY = (int) ev.getY();
		if (action == MotionEvent.ACTION_DOWN) {
			mLastX = mX;
			mLastY = mY;
		}
		mOffsetX = (int) ev.getRawX() - mX;
		mOffsetY = (int) ev.getRawY() - mY;
	}

	public boolean listViewIntercepted() {
		return mListViewIntercepted;
	}

	private boolean mListViewIntercepted = false;

	@Override
	public boolean onInterceptTouchEvent(MotionEvent ev) {
		if (!mDragEnabled) {
			return super.onInterceptTouchEvent(ev);
		}

		saveTouchCoords(ev);
		mLastCallWasIntercept = true;

		int action = ev.getAction() & MotionEvent.ACTION_MASK;

		if (action == MotionEvent.ACTION_DOWN) {
			if (mDragState != IDLE) {
				// intercept and ignore
				mIgnoreTouchEvent = true;
				return true;
			}
			mInTouchEvent = true;
		}

		boolean intercept = false;

		// the following deals with calls to super.onInterceptTouchEvent
		if (mFloatView != null) {
			// super's touch event canceled in startDrag
			intercept = true;
		} else {
			if (super.onInterceptTouchEvent(ev)) {
				mListViewIntercepted = true;
				intercept = true;
			}

			switch (action) {
			case MotionEvent.ACTION_CANCEL:
			case MotionEvent.ACTION_UP:
				doActionUpOrCancel();
				break;
			default:
				if (intercept) {
					mCancelMethod = ON_TOUCH_EVENT;
				} else {
					mCancelMethod = ON_INTERCEPT_TOUCH_EVENT;
				}
			}
		}

		if (action == MotionEvent.ACTION_UP
				|| action == MotionEvent.ACTION_CANCEL) {
			mInTouchEvent = false;
		}

		return intercept;
	}

	/**
	 * Set the width of each drag scroll region by specifying a fraction of the
	 * ListView height.
	 * 
	 * @param heightFraction
	 *            Fraction of ListView height. Capped at 0.5f.
	 * 
	 */
	public void setDragScrollStart(float heightFraction) {
		setDragScrollStarts(heightFraction, heightFraction);
	}

	/**
	 * Set the width of each drag scroll region by specifying a fraction of the
	 * ListView height.
	 * 
	 * @param upperFrac
	 *            Fraction of ListView height for up-scroll bound. Capped at
	 *            0.5f.
	 * @param lowerFrac
	 *            Fraction of ListView height for down-scroll bound. Capped at
	 *            0.5f.
	 * 
	 */
	public void setDragScrollStarts(float upperFrac, float lowerFrac) {
		if (lowerFrac > 0.5f) {
			mDragDownScrollStartFrac = 0.5f;
		} else {
			mDragDownScrollStartFrac = lowerFrac;
		}

		if (upperFrac > 0.5f) {
			mDragUpScrollStartFrac = 0.5f;
		} else {
			mDragUpScrollStartFrac = upperFrac;
		}

		if (getHeight() != 0) {
			updateScrollStarts();
		}
	}

	private void continueDrag(int x, int y) {

		// proposed position
		mFloatLoc.x = x - mDragDeltaX;
		mFloatLoc.y = y - mDragDeltaY;

		doDragFloatView(true);

		int minY = Math.min(y, mFloatViewMid + mFloatViewHeightHalf);
		int maxY = Math.max(y, mFloatViewMid - mFloatViewHeightHalf);

		// get the current scroll direction
		int currentScrollDir = mDragScroller.getScrollDir();

		if (minY > mLastY && minY > mDownScrollStartY
				&& currentScrollDir != DragScroller.DOWN) {
			// dragged down, it is below the down scroll start and it is not
			// scrolling up

			if (currentScrollDir != DragScroller.STOP) {
				// moved directly from up scroll to down scroll
				mDragScroller.stopScrolling(true);
			}

			// start scrolling down
			mDragScroller.startScrolling(DragScroller.DOWN);
		} else if (maxY < mLastY && maxY < mUpScrollStartY
				&& currentScrollDir != DragScroller.UP) {
			// dragged up, it is above the up scroll start and it is not
			// scrolling up

			if (currentScrollDir != DragScroller.STOP) {
				// moved directly from down scroll to up scroll
				mDragScroller.stopScrolling(true);
			}

			// start scrolling up
			mDragScroller.startScrolling(DragScroller.UP);
		} else if (maxY >= mUpScrollStartY && minY <= mDownScrollStartY
				&& mDragScroller.isScrolling()) {
			// not in the upper nor in the lower drag-scroll regions but it is
			// still scrolling

			mDragScroller.stopScrolling(true);
		}
	}

	private void updateScrollStarts() {
		final int padTop = getPaddingTop();
		final int listHeight = getHeight() - padTop - getPaddingBottom();
		float heightF = (float) listHeight;

		mUpScrollStartYF = padTop + mDragUpScrollStartFrac * heightF;
		mDownScrollStartYF = padTop + (1.0f - mDragDownScrollStartFrac)
				* heightF;

		mUpScrollStartY = (int) mUpScrollStartYF;
		mDownScrollStartY = (int) mDownScrollStartYF;

		mDragUpScrollHeight = mUpScrollStartYF - padTop;
		mDragDownScrollHeight = padTop + listHeight - mDownScrollStartYF;
	}

	@Override
	protected void onSizeChanged(int w, int h, int oldw, int oldh) {
		super.onSizeChanged(w, h, oldw, oldh);
		updateScrollStarts();
	}

	private void adjustAllItems() {
		final int first = getFirstVisiblePosition();
		final int last = getLastVisiblePosition();

		int begin = Math.max(0, getHeaderViewsCount() - first);
		int end = Math.min(last - first, getCount() - 1 - getFooterViewsCount()
				- first);

		for (int i = begin; i <= end; ++i) {
			View v = getChildAt(i);
			if (v != null) {
				adjustItem(first + i, v, false);
			}
		}
	}

	private void adjustItem(int position) {
		View v = getChildAt(position - getFirstVisiblePosition());

		if (v != null) {
			adjustItem(position, v, false);
		}
	}

	/**
	 * Sets layout param height, gravity, and visibility on wrapped item.
	 */
	private void adjustItem(int position, View v, boolean invalidChildHeight) {

		// Adjust item height
		ViewGroup.LayoutParams lp = v.getLayoutParams();
		int height;
		if (position != mSrcPos && position != mFirstExpPos
				&& position != mSecondExpPos) {
			height = ViewGroup.LayoutParams.WRAP_CONTENT;
		} else {
			height = calcItemHeight(position, v, invalidChildHeight);
		}

		if (height != lp.height) {
			lp.height = height;
			v.setLayoutParams(lp);
		}

		// Adjust item gravity
		if (position == mFirstExpPos || position == mSecondExpPos) {
			if (position < mSrcPos) {
				((DragSortItemView) v).setGravity(Gravity.BOTTOM);
			} else if (position > mSrcPos) {
				((DragSortItemView) v).setGravity(Gravity.TOP);
			}
		}

		// Finally adjust item visibility

		int oldVis = v.getVisibility();
		int vis = View.VISIBLE;

		if (position == mSrcPos && mFloatView != null) {
			vis = View.INVISIBLE;
		}

		if (vis != oldVis) {
			v.setVisibility(vis);
		}
	}

	private int getChildHeight(int position) {
		if (position == mSrcPos) {
			return 0;
		}

		View v = getChildAt(position - getFirstVisiblePosition());

		if (v != null) {
			// item is onscreen, therefore child height is valid,
			// hence the "true"
			return getChildHeight(position, v, false);
		} else {
			// item is offscreen
			// first check cache for child height at this position
			int childHeight = mChildHeightCache.get(position);
			if (childHeight != -1) {
				// Log.d("mobeta", "found child height in cache!");
				return childHeight;
			}

			final ListAdapter adapter = getAdapter();
			int type = adapter.getItemViewType(position);

			// There might be a better place for checking for the following
			final int typeCount = adapter.getViewTypeCount();
			if (typeCount != mSampleViewTypes.length) {
				mSampleViewTypes = new View[typeCount];
			}

			if (type >= 0) {
				if (mSampleViewTypes[type] == null) {
					v = adapter.getView(position, null, this);
					mSampleViewTypes[type] = v;
				} else {
					v = adapter.getView(position, mSampleViewTypes[type], this);
				}
			} else {
				// type is HEADER_OR_FOOTER or IGNORE
				v = adapter.getView(position, null, this);
			}

			// current child height is invalid, hence "true" below
			childHeight = getChildHeight(position, v, true);

			// cache it because this could have been expensive
			mChildHeightCache.add(position, childHeight);

			return childHeight;
		}
	}

	private int getChildHeight(int position, View item,
			boolean invalidChildHeight) {
		if (position == mSrcPos) {
			return 0;
		}

		View child;
		if (position < getHeaderViewsCount()
				|| position >= getCount() - getFooterViewsCount()) {
			child = item;
		} else {
			child = ((ViewGroup) item).getChildAt(0);
		}

		ViewGroup.LayoutParams lp = child.getLayoutParams();

		if (lp != null) {
			if (lp.height > 0) {
				return lp.height;
			}
		}

		int childHeight = child.getHeight();

		if (childHeight == 0 || invalidChildHeight) {
			measureItem(child);
			childHeight = child.getMeasuredHeight();
		}

		return childHeight;
	}

	private int calcItemHeight(int position, View item,
			boolean invalidChildHeight) {
		return calcItemHeight(position,
				getChildHeight(position, item, invalidChildHeight));
	}

	private int calcItemHeight(int position, int childHeight) {

		int divHeight = getDividerHeight();

		boolean isSliding = mAnimate && mFirstExpPos != mSecondExpPos;
		int maxNonSrcBlankHeight = mFloatViewHeight - mItemHeightCollapsed;
		int slideHeight = (int) (mSlideFrac * maxNonSrcBlankHeight);

		int height;

		if (position == mSrcPos) {
			if (mSrcPos == mFirstExpPos) {
				if (isSliding) {
					height = slideHeight + mItemHeightCollapsed;
				} else {
					height = mFloatViewHeight;
				}
			} else if (mSrcPos == mSecondExpPos) {
				// if gets here, we know an item is sliding
				height = mFloatViewHeight - slideHeight;
			} else {
				height = mItemHeightCollapsed;
			}
		} else if (position == mFirstExpPos) {
			if (isSliding) {
				height = childHeight + slideHeight;
			} else {
				height = childHeight + maxNonSrcBlankHeight;
			}
		} else if (position == mSecondExpPos) {
			// we know an item is sliding (b/c 2ndPos != 1stPos)
			height = childHeight + maxNonSrcBlankHeight - slideHeight;
		} else {
			height = childHeight;
		}

		return height;
	}

	@Override
	public void requestLayout() {
		if (!mBlockLayoutRequests) {
			super.requestLayout();
		}
	}

	private int adjustScroll(int movePos, View moveItem, int oldFirstExpPos,
			int oldSecondExpPos) {
		int adjust = 0;

		final int childHeight = getChildHeight(movePos);

		int moveHeightBefore = moveItem.getHeight();
		int moveHeightAfter = calcItemHeight(movePos, childHeight);

		int moveBlankBefore = moveHeightBefore;
		int moveBlankAfter = moveHeightAfter;
		if (movePos != mSrcPos) {
			moveBlankBefore -= childHeight;
			moveBlankAfter -= childHeight;
		}

		int maxBlank = mFloatViewHeight;
		if (mSrcPos != mFirstExpPos && mSrcPos != mSecondExpPos) {
			maxBlank -= mItemHeightCollapsed;
		}

		if (movePos <= oldFirstExpPos) {
			if (movePos > mFirstExpPos) {
				adjust += maxBlank - moveBlankAfter;
			}
		} else if (movePos == oldSecondExpPos) {
			if (movePos <= mFirstExpPos) {
				adjust += moveBlankBefore - maxBlank;
			} else if (movePos == mSecondExpPos) {
				adjust += moveHeightBefore - moveHeightAfter;
			} else {
				adjust += moveBlankBefore;
			}
		} else {
			if (movePos <= mFirstExpPos) {
				adjust -= maxBlank;
			} else if (movePos == mSecondExpPos) {
				adjust -= moveBlankAfter;
			}
		}

		return adjust;
	}

	private void measureItem(View item) {
		ViewGroup.LayoutParams lp = item.getLayoutParams();
		if (lp == null) {
			lp = new AbsListView.LayoutParams(
					ViewGroup.LayoutParams.FILL_PARENT,
					ViewGroup.LayoutParams.WRAP_CONTENT);
			item.setLayoutParams(lp);
		}
		int wspec = ViewGroup.getChildMeasureSpec(mWidthMeasureSpec,
				getListPaddingLeft() + getListPaddingRight(), lp.width);
		int hspec;
		if (lp.height > 0) {
			hspec = MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY);
		} else {
			hspec = MeasureSpec.makeMeasureSpec(0, MeasureSpec.UNSPECIFIED);
		}
		item.measure(wspec, hspec);
	}

	private void measureFloatView() {
		if (mFloatView != null) {
			measureItem(mFloatView);
			mFloatViewHeight = mFloatView.getMeasuredHeight();
			mFloatViewHeightHalf = mFloatViewHeight / 2;
		}
	}

	@Override
	protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
		super.onMeasure(widthMeasureSpec, heightMeasureSpec);
		// Log.d("mobeta", "onMeasure called");
		if (mFloatView != null) {
			if (mFloatView.isLayoutRequested()) {
				measureFloatView();
			}
			mFloatViewOnMeasured = true; // set to false after layout
		}
		mWidthMeasureSpec = widthMeasureSpec;
	}

	@Override
	protected void layoutChildren() {
		super.layoutChildren();

		if (mFloatView != null) {
			if (mFloatView.isLayoutRequested() && !mFloatViewOnMeasured) {
				// Have to measure here when usual android measure
				// pass is skipped. This happens during a drag-sort
				// when layoutChildren is called directly.
				measureFloatView();
			}
			mFloatView.layout(0, 0, mFloatView.getMeasuredWidth(),
					mFloatView.getMeasuredHeight());
			mFloatViewOnMeasured = false;
		}
	}

	protected boolean onDragTouchEvent(MotionEvent ev) {
		// we are in a drag
		int action = ev.getAction() & MotionEvent.ACTION_MASK;

		switch (ev.getAction() & MotionEvent.ACTION_MASK) {
		case MotionEvent.ACTION_CANCEL:
			if (mDragState == DRAGGING) {
				cancelDrag();
			}
			doActionUpOrCancel();
			break;
		case MotionEvent.ACTION_UP:
			// Log.d("mobeta", "calling stopDrag from onDragTouchEvent");
			if (mDragState == DRAGGING) {
				stopDrag(false);
			}
			doActionUpOrCancel();
			break;
		case MotionEvent.ACTION_MOVE:
			continueDrag((int) ev.getX(), (int) ev.getY());
			break;
		}

		return true;
	}

	private boolean mFloatViewInvalidated = false;

	private void invalidateFloatView() {
		mFloatViewInvalidated = true;
	}

	/**
	 * Start a drag of item at <code>position</code> using the registered
	 * FloatViewManager. Calls through to
	 * {@link #startDrag(int,View,int,int,int)} after obtaining the floating
	 * View from the FloatViewManager.
	 * 
	 * @param position
	 *            Item to drag.
	 * @param dragFlags
	 *            Flags that restrict some movements of the floating View. For
	 *            example, set <code>dragFlags |= 
	 * ~{@link #DRAG_NEG_X}</code> to allow dragging the floating View in all
	 *            directions except off the screen to the left.
	 * @param deltaX
	 *            Offset in x of the touch coordinate from the left edge of the
	 *            floating View (i.e. touch-x minus float View left).
	 * @param deltaY
	 *            Offset in y of the touch coordinate from the top edge of the
	 *            floating View (i.e. touch-y minus float View top).
	 * 
	 * @return True if the drag was started, false otherwise. This
	 *         <code>startDrag</code> will fail if we are not currently in a
	 *         touch event, there is no registered FloatViewManager, or the
	 *         FloatViewManager returns a null View.
	 */
	public boolean startDrag(int position, int dragFlags, int deltaX, int deltaY) {
		if (!mInTouchEvent || mFloatViewManager == null) {
			return false;
		}

		View v = mFloatViewManager.onCreateFloatView(position);

		if (v == null) {
			return false;
		} else {
			return startDrag(position, v, dragFlags, deltaX, deltaY);
		}

	}

	/**
	 * Start a drag of item at <code>position</code> without using a
	 * FloatViewManager.
	 * 
	 * @param position
	 *            Item to drag.
	 * @param floatView
	 *            Floating View.
	 * @param dragFlags
	 *            Flags that restrict some movements of the floating View. For
	 *            example, set <code>dragFlags |= 
	 * ~{@link #DRAG_NEG_X}</code> to allow dragging the floating View in all
	 *            directions except off the screen to the left.
	 * @param deltaX
	 *            Offset in x of the touch coordinate from the left edge of the
	 *            floating View (i.e. touch-x minus float View left).
	 * @param deltaY
	 *            Offset in y of the touch coordinate from the top edge of the
	 *            floating View (i.e. touch-y minus float View top).
	 * 
	 * @return True if the drag was started, false otherwise. This
	 *         <code>startDrag</code> will fail if we are not currently in a
	 *         touch event, <code>floatView</code> is null, or there is a drag
	 *         in progress.
	 */
	public boolean startDrag(int position, View floatView, int dragFlags,
			int deltaX, int deltaY) {
		if (mDragState != IDLE || !mInTouchEvent || mFloatView != null
				|| floatView == null || !mDragEnabled) {
			return false;
		}

		if (getParent() != null) {
			getParent().requestDisallowInterceptTouchEvent(true);
		}

		int pos = position + getHeaderViewsCount();
		mFirstExpPos = pos;
		mSecondExpPos = pos;
		mSrcPos = pos;
		mFloatPos = pos;

		// mDragState = dragType;
		mDragState = DRAGGING;
		mDragFlags = 0;
		mDragFlags |= dragFlags;

		mFloatView = floatView;
		measureFloatView(); // sets mFloatViewHeight

		mDragDeltaX = deltaX;
		mDragDeltaY = deltaY;
		mDragStartY = mY;

		// updateFloatView(mX - mDragDeltaX, mY - mDragDeltaY);
		mFloatLoc.x = mX - mDragDeltaX;
		mFloatLoc.y = mY - mDragDeltaY;

		// set src item invisible
		final View srcItem = getChildAt(mSrcPos - getFirstVisiblePosition());

		if (srcItem != null) {
			srcItem.setVisibility(View.INVISIBLE);
		}

		if (mTrackDragSort) {
			mDragSortTracker.startTracking();
		}

		// once float view is created, events are no longer passed
		// to ListView
		switch (mCancelMethod) {
		case ON_TOUCH_EVENT:
			super.onTouchEvent(mCancelEvent);
			break;
		case ON_INTERCEPT_TOUCH_EVENT:
			super.onInterceptTouchEvent(mCancelEvent);
			break;
		}

		requestLayout();

		if (mLiftAnimator != null) {
			mLiftAnimator.start();
		}

		return true;
	}

	private void doDragFloatView(boolean forceInvalidate) {
		int movePos = getFirstVisiblePosition() + getChildCount() / 2;
		View moveItem = getChildAt(getChildCount() / 2);

		if (moveItem == null) {
			return;
		}

		doDragFloatView(movePos, moveItem, forceInvalidate);
	}

	private void doDragFloatView(int movePos, View moveItem,
			boolean forceInvalidate) {
		mBlockLayoutRequests = true;

		updateFloatView();

		int oldFirstExpPos = mFirstExpPos;
		int oldSecondExpPos = mSecondExpPos;

		boolean updated = updatePositions();

		if (updated) {
			adjustAllItems();
			int scroll = adjustScroll(movePos, moveItem, oldFirstExpPos,
					oldSecondExpPos);
			// Log.d("mobeta", "  adjust scroll="+scroll);

			setSelectionFromTop(movePos, moveItem.getTop() + scroll
					- getPaddingTop());
			layoutChildren();
		}

		if (updated || forceInvalidate) {
			invalidate();
		}

		mBlockLayoutRequests = false;
	}

	/**
	 * Sets float View location based on suggested values and constraints set in
	 * mDragFlags.
	 */
	private void updateFloatView() {

		if (mFloatViewManager != null) {
			mTouchLoc.set(mX, mY);
			mFloatViewManager.onDragFloatView(mFloatView, mFloatLoc, mTouchLoc);
		}

		final int floatX = mFloatLoc.x;
		final int floatY = mFloatLoc.y;

		// restrict x motion
		int padLeft = getPaddingLeft();
		if ((mDragFlags & DRAG_POS_X) == 0 && floatX > padLeft) {
			mFloatLoc.x = padLeft;
		} else if ((mDragFlags & DRAG_NEG_X) == 0 && floatX < padLeft) {
			mFloatLoc.x = padLeft;
		}

		// keep floating view from going past bottom of last header view
		final int numHeaders = getHeaderViewsCount();
		final int numFooters = getFooterViewsCount();
		final int firstPos = getFirstVisiblePosition();
		final int lastPos = getLastVisiblePosition();

		// Log.d("mobeta",
		// "nHead="+numHeaders+" nFoot="+numFooters+" first="+firstPos+" last="+lastPos);
		int topLimit = getPaddingTop();
		if (firstPos < numHeaders) {
			topLimit = getChildAt(numHeaders - firstPos - 1).getBottom();
		}
		if ((mDragFlags & DRAG_NEG_Y) == 0) {
			if (firstPos <= mSrcPos) {
				topLimit = Math.max(getChildAt(mSrcPos - firstPos).getTop(),
						topLimit);
			}
		}
		// bottom limit is top of first footer View or
		// bottom of last item in list
		int bottomLimit = getHeight() - getPaddingBottom();
		if (lastPos >= getCount() - numFooters - 1) {
			bottomLimit = getChildAt(getCount() - numFooters - 1 - firstPos)
					.getBottom();
		}
		if ((mDragFlags & DRAG_POS_Y) == 0) {
			if (lastPos >= mSrcPos) {
				bottomLimit = Math.min(getChildAt(mSrcPos - firstPos)
						.getBottom(), bottomLimit);
			}
		}

		// Log.d("mobeta", "dragView top=" + (y - mDragDeltaY));
		// Log.d("mobeta", "limit=" + limit);
		// Log.d("mobeta", "mDragDeltaY=" + mDragDeltaY);

		if (floatY < topLimit) {
			mFloatLoc.y = topLimit;
		} else if (floatY + mFloatViewHeight > bottomLimit) {
			mFloatLoc.y = bottomLimit - mFloatViewHeight;
		}

		// get y-midpoint of floating view (constrained to ListView bounds)
		mFloatViewMid = mFloatLoc.y + mFloatViewHeightHalf;
	}

	private void destroyFloatView() {
		if (mFloatView != null) {
			mFloatView.setVisibility(GONE);
			if (mFloatViewManager != null) {
				mFloatViewManager.onDestroyFloatView(mFloatView);
			}
			mFloatView = null;
			invalidate();
		}
	}

	/**
	 * Interface for customization of the floating View appearance and dragging
	 * behavior. Implement your own and pass it to {@link #setFloatViewManager}.
	 * If your own is not passed, the default {@link SimpleFloatViewManager}
	 * implementation is used.
	 */
	public interface FloatViewManager {
		/**
		 * Return the floating View for item at <code>position</code>.
		 * DragSortListView will measure and layout this View for you, so feel
		 * free to just inflate it. You can help DSLV by setting some
		 * {@link ViewGroup.LayoutParams} on this View; otherwise it will set
		 * some for you (with a width of FILL_PARENT and a height of
		 * WRAP_CONTENT).
		 * 
		 * @param position
		 *            Position of item to drag (NOTE: <code>position</code>
		 *            excludes header Views; thus, if you want to call
		 *            {@link ListView#getChildAt(int)}, you will need to add
		 *            {@link ListView#getHeaderViewsCount()} to the index).
		 * 
		 * @return The View you wish to display as the floating View.
		 */
		public View onCreateFloatView(int position);

		/**
		 * Called whenever the floating View is dragged. Float View properties
		 * can be changed here. Also, the upcoming location of the float View
		 * can be altered by setting <code>location.x</code> and
		 * <code>location.y</code>.
		 * 
		 * @param floatView
		 *            The floating View.
		 * @param location
		 *            The location (top-left; relative to DSLV top-left) at
		 *            which the float View would like to appear, given the
		 *            current touch location and the offset provided in
		 *            {@link DragSortListView#startDrag}.
		 * @param touch
		 *            The current touch location (relative to DSLV top-left).
		 * @param pendingScroll
		 */
		public void onDragFloatView(View floatView, Point location, Point touch);

		/**
		 * Called when the float View is dropped; lets you perform any necessary
		 * cleanup. The internal DSLV floating View reference is set to null
		 * immediately after this is called.
		 * 
		 * @param floatView
		 *            The floating View passed to
		 *            {@link #onCreateFloatView(int)}.
		 */
		public void onDestroyFloatView(View floatView);
	}

	public void setFloatViewManager(FloatViewManager manager) {
		mFloatViewManager = manager;
	}

	public void setDragListener(DragListener l) {
		mDragListener = l;
	}

	/**
	 * Allows for easy toggling between a DragSortListView and a regular old
	 * ListView. If enabled, items are draggable, where the drag init mode
	 * determines how items are lifted (see {@link setDragInitMode(int)}). If
	 * disabled, items cannot be dragged.
	 * 
	 * @param enabled
	 *            Set <code>true</code> to enable list item dragging
	 */
	public void setDragEnabled(boolean enabled) {
		mDragEnabled = enabled;
	}

	public boolean isDragEnabled() {
		return mDragEnabled;
	}

	/**
	 * This better reorder your ListAdapter! DragSortListView does not do this
	 * for you; doesn't make sense to. Make sure
	 * {@link BaseAdapter#notifyDataSetChanged()} or something like it is called
	 * in your implementation. Furthermore, if you have a choiceMode other than
	 * none and the ListAdapter does not return true for
	 * {@link ListAdapter#hasStableIds()}, you will need to call
	 * {@link #moveCheckState(int, int)} to move the check boxes along with the
	 * list items.
	 * 
	 * @param l
	 */
	public void setDropListener(DropListener l) {
		mDropListener = l;
	}

	/**
	 * Probably a no-brainer, but make sure that your remove listener calls
	 * {@link BaseAdapter#notifyDataSetChanged()} or something like it. When an
	 * item removal occurs, DragSortListView relies on a redraw of all the items
	 * to recover invisible views and such. Strictly speaking, if you remove
	 * something, your dataset has changed...
	 * 
	 * @param l
	 */
	public void setRemoveListener(RemoveListener l) {
		mRemoveListener = l;
	}

	public interface DragListener {
		public void drag(int from, int to);
	}

	/**
	 * Your implementation of this has to reorder your ListAdapter! Make sure to
	 * call {@link BaseAdapter#notifyDataSetChanged()} or something like it in
	 * your implementation.
	 * 
	 * @author heycosmo
	 * 
	 */
	public interface DropListener {
		public void drop(int from, int to);
	}

	/**
	 * Make sure to call {@link BaseAdapter#notifyDataSetChanged()} or something
	 * like it in your implementation.
	 * 
	 * @author heycosmo
	 * 
	 */
	public interface RemoveListener {
		public void remove(int which);
	}

	public interface DragSortListener extends DropListener, DragListener,
			RemoveListener {
	}

	public void setDragSortListener(DragSortListener l) {
		setDropListener(l);
		setDragListener(l);
		setRemoveListener(l);
	}

	/**
	 * Completely custom scroll speed profile. Default increases linearly with
	 * position and is constant in time. Create your own by implementing
	 * {@link DragSortListView.DragScrollProfile}.
	 * 
	 * @param ssp
	 */
	public void setDragScrollProfile(DragScrollProfile ssp) {
		if (ssp != null) {
			mScrollProfile = ssp;
		}
	}

	/**
	 * Use this to move the check state of an item from one position to another
	 * in a drop operation. If you have a choiceMode which is not none, this
	 * method must be called when the order of items changes in an underlying
	 * adapter which does not have stable IDs (see
	 * {@link ListAdapter#hasStableIds()}). This is because without IDs, the
	 * ListView has no way of knowing which items have moved where, and cannot
	 * update the check state accordingly.
	 * <p>
	 * A word of warning about a "feature" in Android that you may run into when
	 * dealing with movable list items: for an adapter that <em>does</em> have
	 * stable IDs, ListView will attempt to locate each item based on its ID and
	 * move the check state from the item's old position to the new position —
	 * which is all fine and good (and removes the need for calling this
	 * function), except for the half-baked approach. Apparently to save time in
	 * the naive algorithm used, ListView will only search for an ID in the
	 * close neighborhood of the old position. If the user moves an item too far
	 * (specifically, more than 20 rows away), ListView will give up and just
	 * force the item to be unchecked. So if there is a reasonable chance that
	 * the user will move items more than 20 rows away from the original
	 * position, you may wish to use an adapter with unstable IDs and call this
	 * method manually instead.
	 * 
	 * @param from
	 * @param to
	 */
	public void moveCheckState(int from, int to) {
		// This method runs in O(n log n) time (n being the number of list
		// items). The bottleneck is the call to AbsListView.setItemChecked,
		// which is O(log n) because of the binary search involved in calling
		// SparseBooleanArray.put().
		//
		// To improve on the average time, we minimize the number of calls to
		// setItemChecked by only calling it for items that actually have a
		// changed state. This is achieved by building a list containing the
		// start and end of the "runs" of checked items, and then moving the
		// runs. Note that moving an item from A to B is essentially a rotation
		// of the range of items in [A, B]. Let's say we have
		// . . U V X Y Z . .
		// and move U after Z. This is equivalent to a rotation one step to the
		// left within the range you are moving across:
		// . . V X Y Z U . .
		//
		// So, to perform the move we enumerate all the runs within the move
		// range, then rotate each run one step to the left or right (depending
		// on move direction). For example, in the list:
		// X X . X X X . X
		// we have two runs. One begins at the last item of the list and wraps
		// around to the beginning, ending at position 1. The second begins at
		// position 3 and ends at position 5. To rotate a run, regardless of
		// length, we only need to set a check mark at one end of the run, and
		// clear a check mark at the other end:
		// X . X X X . X X
		SparseBooleanArray cip = getCheckedItemPositions();
		int rangeStart = from;
		int rangeEnd = to;
		if (to < from) {
			rangeStart = to;
			rangeEnd = from;
		}
		rangeEnd += 1;

		int[] runStart = new int[cip.size()];
		int[] runEnd = new int[cip.size()];
		int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd);
		if (runCount == 1 && (runStart[0] == runEnd[0])) {
			// Special case where all items are checked, we can never set any
			// item to false like we do below.
			return;
		}

		if (from < to) {
			for (int i = 0; i != runCount; i++) {
				setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd),
						true);
				setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd),
						false);
			}

		} else {
			for (int i = 0; i != runCount; i++) {
				setItemChecked(runStart[i], false);
				setItemChecked(runEnd[i], true);
			}
		}
	}

	/**
	 * Use this when an item has been deleted, to move the check state of all
	 * following items up one step. If you have a choiceMode which is not none,
	 * this method must be called when the order of items changes in an
	 * underlying adapter which does not have stable IDs (see
	 * {@link ListAdapter#hasStableIds()}). This is because without IDs, the
	 * ListView has no way of knowing which items have moved where, and cannot
	 * update the check state accordingly.
	 * 
	 * See also further comments on {@link #moveCheckState(int, int)}.
	 * 
	 * @param position
	 */
	public void removeCheckState(int position) {
		SparseBooleanArray cip = getCheckedItemPositions();

		if (cip.size() == 0)
			return;
		int[] runStart = new int[cip.size()];
		int[] runEnd = new int[cip.size()];
		int rangeStart = position;
		int rangeEnd = cip.keyAt(cip.size() - 1) + 1;
		int runCount = buildRunList(cip, rangeStart, rangeEnd, runStart, runEnd);
		for (int i = 0; i != runCount; i++) {
			if (!(runStart[i] == position || (runEnd[i] < runStart[i] && runEnd[i] > position))) {
				// Only set a new check mark in front of this run if it does
				// not contain the deleted position. If it does, we only need
				// to make it one check mark shorter at the end.
				setItemChecked(rotate(runStart[i], -1, rangeStart, rangeEnd),
						true);
			}
			setItemChecked(rotate(runEnd[i], -1, rangeStart, rangeEnd), false);
		}
	}

	private static int buildRunList(SparseBooleanArray cip, int rangeStart,
			int rangeEnd, int[] runStart, int[] runEnd) {
		int runCount = 0;

		int i = findFirstSetIndex(cip, rangeStart, rangeEnd);
		if (i == -1)
			return 0;

		int position = cip.keyAt(i);
		int currentRunStart = position;
		int currentRunEnd = currentRunStart + 1;
		for (i++; i < cip.size() && (position = cip.keyAt(i)) < rangeEnd; i++) {
			if (!cip.valueAt(i)) // not checked => not interesting
				continue;
			if (position == currentRunEnd) {
				currentRunEnd++;
			} else {
				runStart[runCount] = currentRunStart;
				runEnd[runCount] = currentRunEnd;
				runCount++;
				currentRunStart = position;
				currentRunEnd = position + 1;
			}
		}

		if (currentRunEnd == rangeEnd) {
			// rangeStart and rangeEnd are equivalent positions so to be
			// consistent we translate them to the same integer value. That way
			// we can check whether a run covers the entire range by just
			// checking if the start equals the end position.
			currentRunEnd = rangeStart;
		}
		runStart[runCount] = currentRunStart;
		runEnd[runCount] = currentRunEnd;
		runCount++;

		if (runCount > 1) {
			if (runStart[0] == rangeStart && runEnd[runCount - 1] == rangeStart) {
				// The last run ends at the end of the range, and the first run
				// starts at the beginning of the range. So they are actually
				// part of the same run, except they wrap around the end of the
				// range. To avoid adjacent runs, we need to merge them.
				runStart[0] = runStart[runCount - 1];
				runCount--;
			}
		}
		return runCount;
	}

	private static int rotate(int value, int offset, int lowerBound,
			int upperBound) {
		int windowSize = upperBound - lowerBound;

		value += offset;
		if (value < lowerBound) {
			value += windowSize;
		} else if (value >= upperBound) {
			value -= windowSize;
		}
		return value;
	}

	private static int findFirstSetIndex(SparseBooleanArray sba,
			int rangeStart, int rangeEnd) {
		int size = sba.size();
		int i = insertionIndexForKey(sba, rangeStart);
		while (i < size && sba.keyAt(i) < rangeEnd && !sba.valueAt(i))
			i++;
		if (i == size || sba.keyAt(i) >= rangeEnd)
			return -1;
		return i;
	}

	private static int insertionIndexForKey(SparseBooleanArray sba, int key) {
		int low = 0;
		int high = sba.size();
		while (high - low > 0) {
			int middle = (low + high) >> 1;
			if (sba.keyAt(middle) < key)
				low = middle + 1;
			else
				high = middle;
		}
		return low;
	}

	/**
	 * Interface for controlling scroll speed as a function of touch position
	 * and time. Use
	 * {@link DragSortListView#setDragScrollProfile(DragScrollProfile)} to set
	 * custom profile.
	 * 
	 * @author heycosmo
	 * 
	 */
	public interface DragScrollProfile {
		/**
		 * Return a scroll speed in pixels/millisecond. Always return a positive
		 * number.
		 * 
		 * @param w
		 *            Normalized position in scroll region (i.e. w \in [0,1]).
		 *            Small w typically means slow scrolling.
		 * @param t
		 *            Time (in milliseconds) since start of scroll (handy if you
		 *            want scroll acceleration).
		 * @return Scroll speed at position w and time t in pixels/ms.
		 */
		float getSpeed(float w, long t);
	}

	private class DragScroller implements Runnable {

		private boolean mAbort;

		private long mPrevTime;
		private long mCurrTime;

		private int dy;
		private float dt;
		private long tStart;
		private int scrollDir;

		public final static int STOP = -1;
		public final static int UP = 0;
		public final static int DOWN = 1;

		private float mScrollSpeed; // pixels per ms

		private boolean mScrolling = false;

		private int mLastHeader;
		private int mFirstFooter;

		public boolean isScrolling() {
			return mScrolling;
		}

		public int getScrollDir() {
			return mScrolling ? scrollDir : STOP;
		}

		public DragScroller() {
		}

		public void startScrolling(int dir) {
			if (!mScrolling) {
				// Debug.startMethodTracing("dslv-scroll");
				mAbort = false;
				mScrolling = true;
				tStart = SystemClock.uptimeMillis();
				mPrevTime = tStart;
				scrollDir = dir;
				post(this);
			}
		}

		public void stopScrolling(boolean now) {
			if (now) {
				DragSortListView.this.removeCallbacks(this);
				mScrolling = false;
			} else {
				mAbort = true;
			}

			// Debug.stopMethodTracing();
		}

		@Override
		public void run() {
			if (mAbort) {
				mScrolling = false;
				return;
			}

			// Log.d("mobeta", "scroll");

			final int first = getFirstVisiblePosition();
			final int last = getLastVisiblePosition();
			final int count = getCount();
			final int padTop = getPaddingTop();
			final int listHeight = getHeight() - padTop - getPaddingBottom();

			int minY = Math.min(mY, mFloatViewMid + mFloatViewHeightHalf);
			int maxY = Math.max(mY, mFloatViewMid - mFloatViewHeightHalf);

			if (scrollDir == UP) {
				View v = getChildAt(0);
				// Log.d("mobeta", "vtop="+v.getTop()+" padtop="+padTop);
				if (v == null) {
					mScrolling = false;
					return;
				} else {
					if (first == 0 && v.getTop() == padTop) {
						mScrolling = false;
						return;
					}
				}
				mScrollSpeed = mScrollProfile.getSpeed(
						(mUpScrollStartYF - maxY) / mDragUpScrollHeight,
						mPrevTime);
			} else {
				View v = getChildAt(last - first);
				if (v == null) {
					mScrolling = false;
					return;
				} else {
					if (last == count - 1
							&& v.getBottom() <= listHeight + padTop) {
						mScrolling = false;
						return;
					}
				}
				mScrollSpeed = -mScrollProfile.getSpeed(
						(minY - mDownScrollStartYF) / mDragDownScrollHeight,
						mPrevTime);
			}

			mCurrTime = SystemClock.uptimeMillis();
			dt = (float) (mCurrTime - mPrevTime);

			// dy is change in View position of a list item; i.e. positive dy
			// means user is scrolling up (list item moves down the screen,
			// remember
			// y=0 is at top of View).
			dy = (int) Math.round(mScrollSpeed * dt);

			int movePos;
			if (dy >= 0) {
				dy = Math.min(listHeight, dy);
				movePos = first;
			} else {
				dy = Math.max(-listHeight, dy);
				movePos = last;
			}

			final View moveItem = getChildAt(movePos - first);
			int top = moveItem.getTop() + dy;

			if (movePos == 0 && top > padTop) {
				top = padTop;
			}

			// always do scroll
			mBlockLayoutRequests = true;

			setSelectionFromTop(movePos, top - padTop);
			DragSortListView.this.layoutChildren();
			invalidate();

			mBlockLayoutRequests = false;

			// scroll means relative float View movement
			doDragFloatView(movePos, moveItem, false);

			mPrevTime = mCurrTime;
			// Log.d("mobeta", "  updated prevTime="+mPrevTime);

			post(this);
		}
	}

	private class DragSortTracker {
		StringBuilder mBuilder = new StringBuilder();

		File mFile;

		private int mNumInBuffer = 0;
		private int mNumFlushes = 0;

		private boolean mTracking = false;

		public DragSortTracker() {
			File root = Environment.getExternalStorageDirectory();
			mFile = new File(root, "dslv_state.txt");

			if (!mFile.exists()) {
				try {
					mFile.createNewFile();
					Log.d("mobeta", "file created");
				} catch (IOException e) {
					Log.w("mobeta", "Could not create dslv_state.txt");
					Log.d("mobeta", e.getMessage());
				}
			}

		}

		public void startTracking() {
			mBuilder.append("<DSLVStates>\n");
			mNumFlushes = 0;
			mTracking = true;
		}

		public void appendState() {
			if (!mTracking) {
				return;
			}

			mBuilder.append("<DSLVState>\n");
			final int children = getChildCount();
			final int first = getFirstVisiblePosition();
			mBuilder.append("    <Positions>");
			for (int i = 0; i < children; ++i) {
				mBuilder.append(first + i).append(",");
			}
			mBuilder.append("</Positions>\n");

			mBuilder.append("    <Tops>");
			for (int i = 0; i < children; ++i) {
				mBuilder.append(getChildAt(i).getTop()).append(",");
			}
			mBuilder.append("</Tops>\n");
			mBuilder.append("    <Bottoms>");
			for (int i = 0; i < children; ++i) {
				mBuilder.append(getChildAt(i).getBottom()).append(",");
			}
			mBuilder.append("</Bottoms>\n");

			mBuilder.append("    <FirstExpPos>").append(mFirstExpPos)
					.append("</FirstExpPos>\n");
			mBuilder.append("    <FirstExpBlankHeight>")
					.append(getItemHeight(mFirstExpPos)
							- getChildHeight(mFirstExpPos))
					.append("</FirstExpBlankHeight>\n");
			mBuilder.append("    <SecondExpPos>").append(mSecondExpPos)
					.append("</SecondExpPos>\n");
			mBuilder.append("    <SecondExpBlankHeight>")
					.append(getItemHeight(mSecondExpPos)
							- getChildHeight(mSecondExpPos))
					.append("</SecondExpBlankHeight>\n");
			mBuilder.append("    <SrcPos>").append(mSrcPos)
					.append("</SrcPos>\n");
			mBuilder.append("    <SrcHeight>")
					.append(mFloatViewHeight + getDividerHeight())
					.append("</SrcHeight>\n");
			mBuilder.append("    <ViewHeight>").append(getHeight())
					.append("</ViewHeight>\n");
			mBuilder.append("    <LastY>").append(mLastY).append("</LastY>\n");
			mBuilder.append("    <FloatY>").append(mFloatViewMid)
					.append("</FloatY>\n");
			mBuilder.append("    <ShuffleEdges>");
			for (int i = 0; i < children; ++i) {
				mBuilder.append(
						getShuffleEdge(first + i, getChildAt(i).getTop()))
						.append(",");
			}
			mBuilder.append("</ShuffleEdges>\n");

			mBuilder.append("</DSLVState>\n");
			mNumInBuffer++;

			if (mNumInBuffer > 1000) {
				flush();
				mNumInBuffer = 0;
			}
		}

		public void flush() {
			if (!mTracking) {
				return;
			}

			// save to file on sdcard
			try {
				boolean append = true;
				if (mNumFlushes == 0) {
					append = false;
				}
				FileWriter writer = new FileWriter(mFile, append);

				writer.write(mBuilder.toString());
				mBuilder.delete(0, mBuilder.length());

				writer.flush();
				writer.close();

				mNumFlushes++;
			} catch (IOException e) {
				// do nothing
			}
		}

		public void stopTracking() {
			if (mTracking) {
				mBuilder.append("</DSLVStates>\n");
				flush();
				mTracking = false;
			}
		}

	}

}
